/*
 * Copyright (C) 2018 The Asiainfo-Ability Authors
 *
 *      Licensed under the Apache License, Version 2.0 (the "License");
 *      you may not use this file except in compliance with the License.
 *      You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *      Unless required by applicable law or agreed to in writing, software
 *      distributed under the License is distributed on an "AS IS" BASIS,
 *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *      See the License for the specific language governing permissions and
 *      limitations under the License.
 */

package org.asiainfo.ability.gateway.custom;

import com.netflix.zuul.context.RequestContext;
import org.apache.http.Header;
import org.apache.http.HttpRequest;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.Configurable;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.message.BasicHttpRequest;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.asiainfo.ability.base.code.SystemCode;
import org.asiainfo.ability.base.utils.ExceptionUtils;
import org.asiainfo.ability.base.utils.Response;
import org.asiainfo.ability.gateway.context.AbilityRequestContext;

import org.asiainfo.ability.gateway.service.AbilityGatewayFacade;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory;
import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter;
import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException;
import org.springframework.util.MultiValueMap;

import javax.servlet.http.HttpServletRequest;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;

/**
 * Customize the SimpleHostRoutingFilter for dynamic setting url timeout and fallback timeout
 *
 * @Author: visen
 * @Date: 2018/1/16
 * @Description:
 */
public class CustomSimpleHostRoutingFilter extends SimpleHostRoutingFilter {
    public static final Logger logger = LogManager.getLogger(CustomSimpleHostRoutingFilter.class);
    private static final int DEFAULTCONTIME = 3000;

    public CustomSimpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties properties, ApacheHttpClientConnectionManagerFactory connectionManagerFactory, ApacheHttpClientFactory httpClientFactory) {
        super(helper, properties, connectionManagerFactory, httpClientFactory);
        this.helper = helper;
        this.forceOriginalQueryStringEncoding = properties
                .isForceOriginalQueryStringEncoding();
    }

    public CustomSimpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties properties, CloseableHttpClient httpClient) {
        super(helper, properties, httpClient);
        this.helper = helper;
        this.forceOriginalQueryStringEncoding = properties
                .isForceOriginalQueryStringEncoding();
    }

    private boolean forceOriginalQueryStringEncoding;

    private ProxyRequestHelper helper;

    private String getEncodedQueryString(HttpServletRequest request) {
        String query = request.getQueryString();
        return (query != null) ? "?" + query : "";
    }

    private Header[] convertHeaders(MultiValueMap<String, String> headers) {
        List<Header> list = new ArrayList<>();
        for (String name : headers.keySet()) {
            for (String value : headers.get(name)) {
                list.add(new BasicHeader(name, value));
            }
        }
        return list.toArray(new BasicHeader[0]);
    }

    private static class CustomHttpRequest {

        public static interface CustomConfigurable extends Configurable {
            public void setConfig(RequestConfig config);
        }

        public static class BaseHttpRequest extends BasicHttpRequest implements CustomHttpRequest.CustomConfigurable {
            private RequestConfig config;

            @Override
            public RequestConfig getConfig() {
                return config;
            }

            @Override
            public void setConfig(RequestConfig config) {
                this.config = config;
            }

            public BaseHttpRequest(final String method, final String uri) {
                super(method, uri);
            }
        }


        public static class CustomHttpPost extends HttpPost implements CustomHttpRequest.CustomConfigurable {
            private RequestConfig config;

            @Override
            public RequestConfig getConfig() {
                return config;
            }

            @Override
            public void setConfig(RequestConfig config) {
                this.config = config;
            }

            public CustomHttpPost(final String uri) {
                super(uri);
            }
        }

        public static class CustomHttpPut extends HttpPut implements CustomHttpRequest.CustomConfigurable {
            private RequestConfig config;

            @Override
            public RequestConfig getConfig() {
                return config;
            }

            @Override
            public void setConfig(RequestConfig config) {
                this.config = config;
            }

            public CustomHttpPut(final String uri) {
                super(uri);
            }
        }

        public static class CustomHttpPatch extends HttpPatch implements CustomHttpRequest.CustomConfigurable {
            private RequestConfig config;

            @Override
            public RequestConfig getConfig() {
                return config;
            }

            @Override
            public void setConfig(RequestConfig config) {
                this.config = config;
            }

            public CustomHttpPatch(final String uri) {
                super(uri);
            }
        }

        public static class CustomHttpDelete extends BasicHttpEntityEnclosingRequest
                implements CustomHttpRequest.CustomConfigurable {

            private RequestConfig config;

            @Override
            public RequestConfig getConfig() {
                return config;
            }

            @Override
            public void setConfig(RequestConfig config) {
                this.config = config;
            }

            public CustomHttpDelete(final String method, final String uri) {
                super(method, uri);
            }
        }


    }

    @Override
    protected HttpRequest buildHttpRequest(String verb, String uri,
                                           InputStreamEntity entity, MultiValueMap<String, String> headers,
                                           MultiValueMap<String, String> params, HttpServletRequest request) {
        HttpRequest httpRequest;
        String uriWithQueryString = uri + (this.forceOriginalQueryStringEncoding
                ? getEncodedQueryString(request) : this.helper.getQueryString(params));

        switch (verb.toUpperCase()) {
            case "POST":
                HttpPost httpPost = new CustomHttpRequest.CustomHttpPost(uriWithQueryString);
                httpRequest = httpPost;
                httpPost.setEntity(entity);
                break;
            case "PUT":
                HttpPut httpPut = new CustomHttpRequest.CustomHttpPut(uriWithQueryString);
                httpRequest = httpPut;
                httpPut.setEntity(entity);
                break;
            case "PATCH":
                HttpPatch httpPatch = new CustomHttpRequest.CustomHttpPatch(uriWithQueryString);
                httpRequest = httpPatch;
                httpPatch.setEntity(entity);
                break;
            case "DELETE":
                BasicHttpEntityEnclosingRequest entityRequest = new CustomHttpRequest.CustomHttpDelete(
                        verb, uriWithQueryString);
                httpRequest = entityRequest;
                entityRequest.setEntity(entity);
                break;
            default:
                httpRequest = new CustomHttpRequest.BaseHttpRequest(verb, uriWithQueryString);
                logger.debug(uriWithQueryString);
        }

        httpRequest.setHeaders(convertHeaders(headers));
        AbilityRequestContext requestContext = (AbilityRequestContext) RequestContext.getCurrentContext().get(AbilityRequestContext.CONTEXT_KEY);
        if (requestContext != null && requestContext.getTimeout() > 0) {
            RequestConfig.Builder builder = RequestConfig.custom();
            builder.setConnectTimeout(DEFAULTCONTIME);
            builder.setSocketTimeout(requestContext.getTimeout());
            ((CustomHttpRequest.CustomConfigurable) httpRequest).setConfig(builder.build());
        }
        return httpRequest;
    }

    @Autowired
    private AbilityGatewayFacade gatewayFacade;

    @Override
    public Object run() {
        try {
            return super.run();
        } catch (ZuulRuntimeException e) {
            logger.error("", e);

            String error = null, errorSummary = null;
            if (e.getCause().getCause() instanceof SocketTimeoutException) {
                error = Response.error(SystemCode.timeout);
                AbilityRequestContext requestContext = (AbilityRequestContext) RequestContext.getCurrentContext().get(
                        AbilityRequestContext.CONTEXT_KEY);
                if (requestContext != null) {
                    gatewayFacade.setTimeout(requestContext);
                }
                errorSummary = SystemCode.timeout.getName();
            } else if (e.getCause().getCause() instanceof HttpHostConnectException) {
                error = Response.error(SystemCode.invalid_ability);
                errorSummary = SystemCode.invalid_ability.getName();
            } else {
                errorSummary = ExceptionUtils.getTraceInfo(e);
                error = Response.error(SystemCode.unknown);
            }
            RequestContext.getCurrentContext().set("errorSummary", errorSummary);
            RequestContext.getCurrentContext().set("error", error);
        }

        return null;
    }
}
